/*
 * Copyright (c) 2025 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import {ArkFile, ts} from 'arkanalyzer';
import {AdviceChecker, BaseMetaData} from '../BaseChecker';
import {Defects} from '../../Index';
import {AstMatcher, MatcherCallback, MatcherTypes} from '../../Index';
import {Rule} from '../../Index';
import {RuleListUtil} from '../../utils/common/DefectsList';
import {IssueReport} from '../../model/Defects';

const gMetaData: BaseMetaData = {
    severity: 2,
    ruleDocPath: 'docs/no-require-imports.md',
    description: 'A `require()` style import is forbidden.'
};

export class NoRequireImportsCheck implements AdviceChecker {
    readonly metaData: BaseMetaData = gMetaData;
    public rule: Rule;
    public defects: Defects[] = [];
    public issues: IssueReport[] = [];
    private requireDefined = false; // whether `require` is a user-defined function
    private fileChecked = false; // Whether the file has already been checked for a custom `require` function
    private readonly REQUIRE_NAME: string = 'require';
    public beforeCheck(): void {
    }

    public afterCheck(): void {
        this.requireDefined = false;
        this.fileChecked = false;
    }

    private createMatcher = (predicate: (node: ts.Node) => boolean): AstMatcher => ({
        matcherType: MatcherTypes.AST,
        match: predicate
    });

    private readonly matchers = {
        callExpression: this.createMatcher(ts.isCallExpression),
        externalModuleReference: this.createMatcher(ts.isExternalModuleReference)
    };

    public registerMatchers(): MatcherCallback[] {
        return [
            { matcher: this.matchers.callExpression, callback: this.checkCallExpression },
            { matcher: this.matchers.externalModuleReference, callback: this.checkExternalModuleReference }
        ];
    }

    // Required by AdviceChecker interface
    public check = (): void => {
    }

    // require(…)
    private checkCallExpression = (node: ts.CallExpression, targetFile: ArkFile): void => {
        if (this.requireDefined) {
            return;
        }

        if (!this.fileChecked) {
            this.requireDefined = this.hasCustomRequire(targetFile);
            this.fileChecked = true;
        }

        const expr = node.expression;
        if (ts.isIdentifier(expr) && expr.text === this.REQUIRE_NAME && !this.requireDefined) {
            this.reportIssue(targetFile, node);
        }
    };

    private hasCustomRequire(targetFile: ArkFile): boolean {
        for (const arkMethod of targetFile.getDefaultClass().getMethods()) {
            if (arkMethod.getName() === this.REQUIRE_NAME) {
                return true;
            }
        }

        for (const info of targetFile.getImportInfos()) {
            if (info.getImportClauseName() === this.REQUIRE_NAME) {
                return true;
            }
        }

        return false;
    }

    // import lib3 = require('./lib3')
    private checkExternalModuleReference = (node: ts.ExternalModuleReference, targetFile: ArkFile): void => {
        this.reportIssue(targetFile, node);
    };

    private reportIssue(targetFile: ArkFile, node: ts.Node): void {
        const sourceFile = node.getSourceFile();
        const startPosition = sourceFile.getLineAndCharacterOfPosition(node.getStart());
        const startLine = startPosition.line + 1;
        const startCol = startPosition.character + 1;
        this.addIssueReport(targetFile, startLine, startCol, 0, gMetaData.description);
    }

    private addIssueReport(arkFile: ArkFile, line: number, startCol: number, endCol: number, message: string): void {
        const severity = this.rule.alert ?? this.metaData.severity;
        const filePath = arkFile.getFilePath();
        const defect = new Defects(line, startCol, endCol, message, severity, this.rule.ruleId,
            filePath, this.metaData.ruleDocPath, true, false, false);
        this.issues.push(new IssueReport(defect, undefined));
        RuleListUtil.push(defect);
    }
}